<?php

declare(strict_types=1);

namespace Vipkwd\SDK\OAuth;

use Vipkwd\SDK\OAuth\Dependents\Http;
use Vipkwd\SDK\OAuth\Storage\Db as Cache;
use Vipkwd\SDK\OAuth\Action as OAuthAction;
use \Exception;

/*
$instance = new \Vipkwd\SDK\OAuth\OAuth($options);

1、授权码（authorization code）方式
    1.1、获取登录链接: $instance->getLoginUrl('code');
    1.2、捕获code换取token:

        1.2.a、自动拉取token用户信息: response ["tokenData"=> array, "userInfo" => array]
            $response = $instance->authorizeCodeType(true);

        1.2.b、自动拉取token用户信息: response [...$tokenData]
            $response = $instance->authorizeCodeType(true, function (array $tokenData, array &$userInfo, string $clientId) use ($session):void {
                $userInfo['age'] = 18;
                $userInfo['birthday'] = 20180101;

                $session->set('oauth', $tokenData);
                $session->set('user', $userInfo);
            });
        1.2.c、不拉取token用户信息: response [...$tokenData]
            $response = $instance->authorizeCodeType(false, function (array $tokenData, array $userInfo = [], string $clientId) use ($session):void{
                $session->set('oauth', $tokenData);
                // $userInfo === []
            });

2、隐藏式（implicit）
    2.1、获取登录链接: $instance->getLoginUrl('token');
    2.2、授权成功,在回调地址中，用JS捕获地址锚点(注意，令牌的位置是URL锚点（fragment），而不是查询字符串（querystring），这是因为 OAuth 2.0 允许跳转网址是 HTTP 协议，因此存在"中间人攻击"的风险，而浏览器跳转时，锚点不会发到服务器，就减少了泄漏令牌的风险。)

3、凭证式（client credentials）:适用于没有前端的应用(命令行)
    注意：这种方式请求地址会暴露APP_SECRET，且给出的令牌，是针对第三方应用的，而不是针对用户的，即有可能多个用户共享同一个令牌。
    3.1、获取凭证式授权post url: $instance->getTypesUrl('client');请求捕获JSON响应
        或者:
        服务端用 clientCredentialsType(bool $autoFetchUserInfo = false, ?\Closure $callback = null) 方法处理（同授权码方式 1.2.xxx ）


4 、密码式（password）
    4.1、获取凭证式授权post url: $instance->getTypesUrl('password');请求捕获JSON响应
        或者:
        服务端用 passwordType(string $username, string $password, bool $autoFetchUserInfo = false, ?\Closure $callback = null) 方法处理（同授权码方式 1.2.xxx ）

*/

class OAuth
{
    private $options = [];
    private $except = null;
    private $tokenData = [];
    private $token_id = 0;
    private $cacheTable = 'token';
    private static $version = 'v1';
    private static $oauthServer = 'http://oauth.hosts.run/v1';
    private static $apiServer = 'http://api.hosts.run/v1';
    private static $sessionFile = __DIR__ . '/token.session';

    static $sdk;

    public function __construct(array $options)
    {
        $this->options = array_merge([
            'auto_save_cache' => false,
            'cache_save_path' => __DIR__ .'/cache',
	        'oauth_server' => $options['server_url'] ?? '',
	        'api_server' => $options['server_url'] ?? '',
	        
            'client_id' => '',
            'redirect_uri' => '',
            'client_secret' => '',
            'response_type' => 'code',
            'scope' => 'basic',
            'state' => 'xyz',
        ], $options);

        if ($this->options['oauth_server']) {
            self::$oauthServer = $this->options['oauth_server'];
        }
        if ($this->options['api_server']) {
            self::$apiServer = $this->options['api_server'];
        }
        if (!self::$sdk)
            self::$sdk = $this;
    }

    /**
     * 生成 授权码/简化 模式URL
     * 
     * @param string $type <code|token> code:授权码模式；token:授权码隐藏式
     * @param string|null $scope <basic>
     * @param string|null $state <xyz>
     * 
     * @return string
     */
    public function getLoginUrl(string $type = 'code', string $scope = null, string $state = null)
    {
        $param = function ($value, $mapKey) {
            return $value ? $value : (isset($this->options[$mapKey]) ? $this->options[$mapKey] : '');
        };
        return implode('/', [
            self::$oauthServer,
            // self::$version,
            sprintf(
                'oauth/authorize?client_id=%s&response_type=%s&scope=%s&state=%s&redirect_uri=%s',
                $param(null, 'client_id'),
                strtolower($type) !== 'token' ? 'code' : 'token',
                $param($scope, 'scope'),
                $param($state, 'state'),
                $param(null, 'redirect_uri'),
            )
        ]);
    }

    /**
     * 获取 凭证式/密码 模式URL
     * 
     * @param string $type <password|client> password:密码模式；client:凭证式
     * @param string|null $username 密码模式参数
     * @param string|null $password 密码模式参数
     * 
     * @return string
     */
    public function getTypesUrl(string $type = 'password', string $username = null, string $password = null)
    {
        if (strtolower($type) == 'password') {
            $str = sprintf(
                'oauth/token?grant_type=password&username=%s&password=%s&client_id=%s&client_secret=%s',
                $username,
                $password,
                $this->options['client_id'],
                $this->options['client_secret'],
            );
        } else {
            $str = sprintf(
                'oauth/token?grant_type=client_credentials&client_id=%s&client_secret=%s',
                $this->options['client_id'],
                $this->options['client_secret'],
            );
        }
        return implode('/', [
            self::$oauthServer,
            // self::$version,
            $str
        ]);
    }

    /**
     * 接收用户中心返回的授权码
     * 
     * @throws \Exception
     * @return void|array
     */
    public function authorizeCodeType(bool $autoFetchUserInfo = false, \Closure $callback = null)
    {

        if (($code = $this->getReturnCode()) !== null) {
            $res = $this->getOAuthTokenRemote($autoFetchUserInfo, $code, $callback);
            if (is_array($res))
                return $res;
        } else {
            $this->except = [
                "error" => "invalid_callback",
                "error_description" => "Authorization code doesn't exist or is invalid for the client"
            ];
        }
    }

    /**
     * 认证模式刷新令牌
     * 
     * @throws \Exception
     * @return void|array
     */
    public function refreshToken(string $refresh_token, bool $autoFetchUserInfo = false, \Closure $callback = null)
    {
        if (
            is_array($res = $this->getOAuthTokenRemote($autoFetchUserInfo, null, $callback, [
                'grant_type' => 'refresh_token',
                'refresh_token' => $refresh_token,
                'redirect_uri' => null,
                'client_id' => $this->options['client_id'],
                'client_secret' => $this->options['client_secret'],
            ]))
        ) {
            return $res;
        }
    }

    /**
     * 凭证模式获取令牌
     * 
     * 主意：此种方式给出的令牌，是针对第三方应用的，而不是针对用户的，即有可能多个用户共享同一个令牌。
     * 令牌使用：请求头信息，加上一个Authorization字段： curl -H "Authorization: Bearer ACCESS_TOKEN" "https://api.b.com"
     * 
     * @throws \Exception
     * @return void|array
     */
    public function clientCredentialsType(bool $autoFetchUserInfo = false, \Closure $callback = null)
    {
        $res = $this->getOAuthTokenRemote($autoFetchUserInfo, null, $callback, [
            'grant_type' => 'client_credentials',
            'code' => null,
            'redirect_uri' => null,
            'client_id' => $this->options['client_id'],
            'client_secret' => $this->options['client_secret'],
        ]);
        if (is_array($res)) {
            return $res;
        }
    }

    /**
     * 密码模式获取令牌
     * 
     * 主意：此种方式给出的令牌，是针对第三方应用的，而不是针对用户的，即有可能多个用户共享同一个令牌。
     * 令牌使用：请求头信息，加上一个Authorization字段： curl -H "Authorization: Bearer ACCESS_TOKEN" "https://api.b.com"
     * 
     * @throws \Exception
     * @return void|array
     */
    public function passwordType(string $username, string $password, bool $autoFetchUserInfo = false, \Closure $callback = null)
    {
        $res = $this->getOAuthTokenRemote($autoFetchUserInfo, null, $callback, [
            'grant_type' => 'password',
            'username' => $username,
            'password' => $password,
            'client_id' => $this->options['client_id'],
            'client_secret' => $this->options['client_secret'],

            'redirect_uri' => null,
            'code' => null,
        ]);
        if (is_array($res)) {
            return $res;
        }
    }

    /**
     * @param string $method [get|post|put|delete]
     * @param string $service 服务资源名称
     * @param string|integer $resourceId <''>
     * @param array $data 附加请求数据
     * 
     * @param null|array
     * 
     */
    public function resource(string $access_token, string $api, string $method = 'get', array $data = [])
    {
        $url = rtrim(self::$apiServer, '/') . '/' . trim($api, '/') . sprintf('?access_token=%s', $access_token);
        $header = ['service' => sprintf('%s ' . trim($api, '/'), strtolower($method))];
        switch (strtoupper($method)) {
            case "POST":
                return Http::post($url, $data, 'json', $header);
            case "DELETE":
                return Http::delete($url, $data, 'json', $header);
            case "PUT":
                return Http::put($url, $data, 'json', $header);
            case "GET":
                return Http::get($url, $data, $header);
            case "OPTIONS":
                return Http::options($url, $data, 'json', $header);
            case "PATCH":
                return Http::patch($url, $data, 'json', $header);
        }
        return null;
    }

    public function __get(string $property)
    {
        switch ($property) {
            case "clientId":
            case "client_id":
                return $this->options['client_id'];

            case "except":
            case "exception":
                return $this->except;
            case "oauthVersion":
            case "version":
                return self::$version;
            case "oauthServer":
            case "server":
                return self::$oauthServer;
            case "apiServer":
            case "api":
                return self::$apiServer;
            default:
                return null;
        }
    }

    /**
     * 获取本地缓存token信息
     * 
     * @param string $field
     * @param mixed $defaults $field不存在时返回$default值
     * 
     * @throws \Exception
     * @return array|mixed
     */
    private function getTokenCache(string $field = null, $defaults = null)
    {
        $token = !empty($this->tokenData) ? $this->tokenData : $this->cacheInstance()->find($this->token_id); //  (json_decode(@\file_get_contents(self::$sessionFile), true) ?? []);
        if ($field) {
            return isset($token[$field]) ? $token[$field] : $defaults;
        }
        return $token;
    }

    /**
     * 捕获手动授权后回调地址中的 code
     * 
     * @return string|null
     */
    private function getReturnCode()
    {
        $referer = true;
        if (isset($_SERVER['HTTP_REFERER'])) {
            $referer = strripos($_SERVER['HTTP_REFERER'] ?? '', self::$oauthServer) !== false;
        }
        if (isset($_GET['code']) && isset($_GET['state']) && $_SERVER['REQUEST_URI'] && $referer) {
            //将认证服务器返回的授权码从 URL 中解析出来
            return substr($_SERVER['REQUEST_URI'], strpos($_SERVER['REQUEST_URI'], 'code=') + 5, 40);
        }
        return null;
    }
    /**
     * 步骤4 拿授权码去申请令牌
     * 
     * @param bool $autoFetchUserInfo 是否自动刷新用户信息
     * @param string|null $authorizationCode 授权码|刷新token时此值为null
     * @param \Closure $callback
     * @param array $data 附加参数,请示前会合并到接口参数队列
     * 
     * @throws \Exception
     * @return array|null
     */
    private function getOAuthTokenRemote(bool $autoFetchUserInfo, ?string $authorizationCode = null, \Closure $callback = null, array $data = [])
    {
        try {
            $userInfo = [];
            // 步骤4 拿授权码去申请令牌
            $tokenData = $this->OAuthToken($authorizationCode, $data);
            if ($autoFetchUserInfo && isset($tokenData['user_id'])) {
                $userInfo = $this->resource($tokenData['access_token'], '/user/' . $tokenData['user_id'] ?? '');
                //devdump($userInfo,1);
                $userInfo = isset($userInfo['result']) ? $userInfo['result']['data'] : $userInfo['data'];

                // $userInfo['___'] = Cache::table($this->cacheTable)->selectAll(); //($userInfo['id']);
            }
            //devdump($userInfo,1);

            // 放在$callback 之前，以此兼容callback 中主动重置cache
            if (isset($this->options['auto_save_cache']) && $this->options['auto_save_cache']) {
                OAuthAction::cacheOAuthUser($userInfo, $this->options['client_id']);
                OAuthAction::cacheOAuthToken($tokenData, $this->options['client_id'], false);
            }
            if (is_callable($callback)) {
                //TODO 阻止外部引用赋值
                $_tokenData = array_merge([], $tokenData, []);
                $clientId = strval($this->options['client_id']);

                $callback($_tokenData, $userInfo, $clientId);
                unset($_tokenData, $clientId);
            }
            return [
                'clientId' => $this->options['client_id'],
                'tokenData' => $tokenData,
                'userInfo' => $userInfo,
            ];
        } catch (Exception $e) {
            $this->except = [
                "error" => "Server Error",
                "error_description" => $e->getMessage()
            ];
            return $this->except;
        }
    }

    /**
     * 远程获取token
     * 
     * @param string|null $authorize_code
     * 
     * @throws \Exception
     * @return array
     */
    private function OAuthToken(string $authorize_code = null, array $data = [])
    {
        $data = array_merge([
            'grant_type' => 'authorization_code',
            'code' => $authorize_code,
            'redirect_uri' => $this->options['redirect_uri'],
        ], $data);

        // 步骤4 拿授权码去申请令牌
        $response = Http::post(rtrim(self::$oauthServer, '/') . '/oauth/token', http_build_query($data), 'form', [
            'auth' => [$this->options['client_id'], $this->options['client_secret'] ,'basic'],
        ], false);
        
        return $this->exceptionHandler($response, function (&$res) use ($data) {
            if (is_array($res)) {
                @date_default_timezone_set('PRC');
                $res['expires_time'] = $res['expires_in'] ? (time() + $res['expires_in'] - 5) : -1;

                //刷新TOKEN时服务端没有响应 refresh_token字段
                if ($data['grant_type'] == 'refresh_token') {
                    $res['refresh_token'] = $data['refresh_token'];
                }
                ksort($res);
                // @\file_put_contents(self::$sessionFile, json_encode($res));

                $this->cacheInstance()->where('user_id', $res['user_id'])->delete();
                $this->cacheInstance()->where('expires_time', '<', time())->delete();

                $res['token_id'] = $this->cacheInstance()->insertGetId($res);
                // $this->tokenData = $res;
            }
        });
    }

    private function exceptionHandler($response, callable $callback)
    {
        if (!is_array($response)) {
            $response = [
                "error" => "server error!",
                "error_description" => is_bool($response) ? $response : $response,
            ];
        }
        if (isset($response['error']) && isset($response['error_description'])) {
            $this->except = $response;
            throw new Exception($response['error_description']);
        }
        $callback($response);
        return $response;
    }

    private function cacheInstance()
    {
        return Cache::instance(['path' => $this->options['cache_save_path']])->table($this->cacheTable);
    }
}
